/* 静态资源-HTML 检测策略 */
const { JSDOM } = require('jsdom');
const CONSTS = require('@lib/consts/index');
const Report = require('@lib/utils/report');
const Fs = require('@lib/utils/fs');
const Dom = require('@lib/utils/dom');
const {
  isValuableArray,
  mergeArrayByField,
  escapeHTML
} = require('@lib/utils/tools');

// 需要写入报告文件的数据
const outputList = [];

class SrHtml {
  /**
   * 执行策略的函数
   * @param {*} htmlList html文件列表
   * @param {*} extra 额外参数
   * @param {*} cb 回调函数
   */
  static async execute(htmlList = [], extra, cb) {
    if (!isValuableArray(htmlList)) {
      cb && cb();
      return;
    }
    const { cssList } = extra;
    // 1.按照文件遍历
    for (const item of htmlList) {
      const { filePath } = item;
      const htmlContent = Fs.readFileSync(filePath);
      const dom = new JSDOM(htmlContent);
      const win = dom.window;
      const doc = win.document;
      // 2.策略开始
      SrHtml.detectFavicon(item, doc, outputList);
      await SrHtml.detectImgTag(item, doc, win, cssList, outputList);
      SrHtml.detectScriptPos(item, doc, outputList);
      SrHtml.detectDomNestedDepth(item, doc, outputList);
      SrHtml.detectDomInvalidStructure(item, doc, outputList);
    }
    // 3.整合outputList相同title项
    const newOutputList = mergeArrayByField(outputList, 'title', 'data');
    // 4.输出数据到报告目录下的临时文件
    if (!newOutputList.length) {
      cb && cb();
      return;
    }
    const { index } = extra;
    Report.outputTempFile(
      newOutputList,
      '静态资源-HTML',
      `${index}sr-html-${CONSTS.REPORT_TEMP_FILENAME}`,
      () => {
        cb && cb();
      }
    );
  }

  /**
   * 策略1：检测html文件中是否有favicon.ico，如果不设置favicon.ico控制台会报错
   * @param {*} htmlItem html列表每一项
   * @param {*} doc document对象
   * @param {*} outputList 报告文件的数据列表
   */
  static detectFavicon(htmlItem, doc, outputList = []) {
    const iconName = 'favicon.ico';
    // 1.查找<link>标签
    const linkTags = doc.querySelectorAll('link');
    let hasFavicon = false;
    // 2.遍历<link>标签，检查是否存在使用favicon.ico的<link> 标签
    // 这里不做项目中是否存在favicon.ico的判断，因为可能favicon.ico存放在cdn上
    for (let i = 0, len = linkTags.length; i < len; i++) {
      const relAttr = linkTags[i].getAttribute('rel');
      const hrefAttr = linkTags[i].getAttribute('href');
      if (
        relAttr &&
        (relAttr.includes('icon') || relAttr.includes('shortcut icon')) &&
        hrefAttr &&
        hrefAttr.toLowerCase().includes(iconName)
      ) {
        hasFavicon = true;
        break;
      }
    }
    if (!hasFavicon) {
      Report.formatAndpushReportData(
        [{
          fileName: htmlItem.fileName,
          filePath: htmlItem.filePath
        }],
        'HTML页面缺少favicon.ico',
        outputList
      );
    }
  }

  /**
   * 策略2：检测img标签是否设置width或height属性，未设置会引起浏览器重新布局影响页面加载速度-包括：
   * 标签上设置width或height属性
   * 标签上的style属性中设置width或height
   * 内嵌文件style设置width或height
   * 外部样式表CSS文件(包括外部cdn引入和项目文件引入)设置width或height
   * @param {*} htmlItem html列表每一项
   * @param {*} doc document对象
   * @param {*} win windoe对象
   * @param {*} cssList 检测img标签的属性需要用到css文件列表
   * @param {*} outputList 报告文件的数据列表
   */
  static async detectImgTag(htmlItem, doc, win, cssList, outputList = []) {
    const imgTags = doc.getElementsByTagName('img');
    let isValidImgTag = true;
    for (const imgTag of imgTags) {
      isValidImgTag = await Dom.validateImgWorH(imgTag, doc, win, cssList);
      // 存在未设置width或height属性的img标签
      if (!isValidImgTag) {
        Report.formatAndpushReportData(
          [{
            fileName: htmlItem.fileName,
            filePath: htmlItem.filePath
          }],
          escapeHTML('存在未设置width或height属性的<img>标签'),
          outputList
        );
        break;
      }
    }
  }

  /**
   * 策略3：检测script脚本放置的位置，如果放置在<head>标签中就需要使用async或defer属性
   * @param {*} htmlItem html列表每一项
   * @param {*} doc document对象
   * @param {*} outputList 报告文件的数据列表
   */
  static detectScriptPos(htmlItem, doc, outputList = []) {
    // 1.查找<script>标签
    const scripts = doc.getElementsByTagName('script');
    let hasInvalidScript = false;
    // 2.遍历<script>标签，检查其是否存在放置在<head>标签的情况
    // 有的话就需要看该<script>标签是否使用async或defer属性，没使用则视为影响性能
    for (let i = 0, len = scripts.length; i < len; i++) {
      const script = scripts[i];
      if (
        script.src &&
        script.src !== '' &&
        script.parentNode.nodeName === 'HEAD' &&
        (!script.hasAttribute('async') && !script.hasAttribute('defer'))
      ) {
        hasInvalidScript = true;
        break;
      }
    }
    if (hasInvalidScript) {
      Report.formatAndpushReportData(
        [{
          fileName: htmlItem.fileName,
          filePath: htmlItem.filePath
        }],
        escapeHTML('存在放置在<head>标签的script脚本且没有使用async或defer属性'),
        outputList
      );
    }
  }

  /**
   * 策略4：检测是否存在过深的DOM嵌套
   * @param {*} htmlItem html列表每一项
   * @param {*} doc document对象
   * @param {*} outputList 报告文件的数据列表
   */
  static detectDomNestedDepth(htmlItem, doc, outputList = []) {
    // 从<body>开始往下找
    const bodyElement = doc.body;
    const depthLimit = CONSTS.DOM_DEPTH_LIMIT;
    const { isExceed, element } = Dom.checkNestedDepth(bodyElement, 1, depthLimit);
    if (isExceed) {
      Report.formatAndpushReportData(
        [{
          fileName: htmlItem.fileName,
          filePath: htmlItem.filePath,
          warnMsg: `【!尽量避免】存在${element.tagName.toLowerCase()}标签嵌套过深`
        }],
        `存在超过${depthLimit}层的DOM嵌套`,
        outputList
      );
    }
  }

  /**
   * 策略5：检测是否存在使用错误的DOM节点或冗余标签
   * @param {*} htmlItem html列表每一项
   * @param {*} doc document对象
   * @param {*} outputList 报告文件的数据列表
   */
  static detectDomInvalidStructure(htmlItem, doc, outputList = []) {
    // 从<body>开始往下找
    const bodyElement = doc.body;
    const errList = [];
    Dom.traverseCheckStructure(bodyElement, errList);
    if (isValuableArray(errList)) {
      Report.formatAndpushReportData(
        errList.map(item => ({
          fileName: htmlItem.fileName,
          filePath: htmlItem.filePath,
          warnMsg: item.warnMsg
        })),
        '存在使用错误的DOM节点或冗余标签',
        outputList
      );
    }
  }
}
module.exports = SrHtml;
